package com.gitee.wsl.prism4j

import com.gitee.wsl.ext.find
import com.gitee.wsl.ext.group
import com.gitee.wsl.ext.start


class Prism4j(private val grammarLocator: GrammarLocator = MyGrammarLocator()) {
    interface Grammar {
        fun name(): String

        // should mention that returned array is mutable
        fun tokens(): MutableList<Token>
    }

    interface Token {
        fun name(): String

        fun patterns(): MutableList<Pattern>

        companion object{
            val EMPTY = TokenImpl(
                name = "EMPTY",
                patterns = mutableListOf()
            )
        }
    }

    interface Pattern {
        fun regex(): Regex

        fun lookbehind(): Boolean

        fun greedy(): Boolean

        fun alias(): String?

        fun inside(): Grammar?

        companion object{
            val CASE_INSENSITIVE = RegexOption.IGNORE_CASE
            val MULTILINE = RegexOption.MULTILINE
            val UNIX_LINES = RegexOption.UNIX_LINES
            val LITERAL = RegexOption.LITERAL
            val CANON_EQ = RegexOption.CANON_EQ

            fun compile(regex:String) = regex.toRegex()
            fun compile(regex:String,option: RegexOption) = regex.toRegex(option)
            fun compile(regex:String,options: Set<RegexOption>) = regex.toRegex(options)
        }
    }

    /**
     * Basic structure that represents parsing state
     *
     * @see Text
     *
     * @see Syntax
     */
    interface Node {
        /**
         * @return raw text length. For [Text] node it\'s [Text.literal] length
         * and for [Syntax] it is [Syntax.matchedString] length
         */
        fun textLength(): Int

        /**
         * As we have only two types maybe doing a lot of `instanceof` checks is not that required
         *
         * @return a boolean indicating if this node is an instance of [Syntax]
         */
        val isSyntax: Boolean
    }

    interface Text : Node {
        fun literal(): String
    }

    interface Syntax : Node {

        override val isSyntax: Boolean get() = true

        fun type(): String

        fun children(): MutableList<out Node>

        fun alias(): String?

        fun matchedString(): String

        fun greedy(): Boolean

        /**
         * The main aim for this flag is to be able to properly construct simplified
         * array of tokens during tests. If it\'s set to true, then children will be
         * inside another array. Otherwise they will be _flattened_ into the same array
         * as token type information
         *
         * @return a flag indicating if children of this node were tokenized
         */
        fun tokenized(): Boolean
    }

    /**
     * @see AbsVisitor
     */
    interface Visitor {
        fun visit(nodes: List<Node>)
    }

    fun tokenize(text: String, grammar: Grammar): MutableList<Node> {
        val entries: MutableList<Node> = ArrayList(3)
        entries.add(TextImpl(text))
        matchGrammar(text, entries, grammar, 0, 0, false, null)
        return entries
    }

    fun grammar(name: String): Grammar? {
        return grammarLocator.grammar(this, name)
    }

    private fun matchGrammar(
        text: String,
        entries: MutableList<Node>,
        grammar: Grammar,
        index: Int,
        startPosition: Int,
        oneShot: Boolean,
        target: Token?,
    ) {
        val textLength = text.length

        for (token in grammar.tokens()) {

            if (token === target) {
                return
            }

            for (pattern in token.patterns()) {
                val lookbehind = pattern.lookbehind()
                val greedy = pattern.greedy()
                var lookbehindLength = 0

                val regex = pattern.regex()

                // Don't cache textLength as it changes during the loop
                var i = index
                var position = startPosition
                while (i < entries.size) {
                    if (entries.size > textLength) {
                        throw RuntimeException(
                            "Prism4j internal error. Number of entry nodes " +
                                    "is greater that the text length.\n" +
                                    "Nodes: " + entries + "\n" +
                                    "Text: " + text
                        )
                    }

                    val node = entries.get(i)
                    if (isSyntaxNode(node)) {
                        position += entries.get(i).textLength()
                        ++i
                        continue
                    }

                    var str = (node as Text).literal()

                    val matcher: MatchResult?
                    val deleteCount: Int
                    val greedyMatch: Boolean
                    var greedyAdd = 0

                    if (greedy && i != entries.size - 1) {
                        matcher = regex.find(text,position)
                        // limit search to the position (?)
                        //matcher.region(position, textLength)

                        if ( matcher==null || !matcher.find()) {
                            break
                        }

                        var from = matcher.start()

                        if (lookbehind) {
                            from += matcher.group(1)?.length?:0
                        }
                        val to: Int = matcher.start() + (matcher.group(0)?.length?:0)

                        var k = i
                        var p = position

                        val len = entries.size
                        while (k < len && (p < to || (!isSyntaxNode(entries.get(k)) && !isGreedyNode(entries.get(k - 1))))) {
                            p += entries.get(k).textLength()
                            // Move the index i to the element in strarr that is closest to from
                            if (from >= p) {
                                i += 1
                                position = p
                            }
                            ++k
                        }

                        if (isSyntaxNode(entries.get(i))) {
                            position += entries.get(i).textLength()
                            ++i
                            continue
                        }

                        deleteCount = k - i
                        str = text.substring(position, p)
                        greedyMatch = true
                        greedyAdd = -position
                    } else {
                        matcher = regex.find(str)
                        deleteCount = 1
                        greedyMatch = false
                    }

                    if ( matcher==null || !greedyMatch && !matcher.find()) {
                        if (oneShot) {
                            break
                        }
                        position += entries.get(i).textLength()
                        ++i
                        continue
                    }

                    if (lookbehind) {
                        val group = matcher.group(1)
                        lookbehindLength = if (group != null) group.length else 0
                    }

                    val from = matcher.start() + greedyAdd + lookbehindLength
                    val match: String
                    if (lookbehindLength > 0) {
                        match = matcher.group()!!.substring(lookbehindLength)
                    } else {
                        match = matcher.group()!!
                    }
                    val to = from + match.length

                    for (d in 0..<deleteCount) {
                        entries.removeAt(i)
                    }

                    var i2 = i

                    if (from != 0) {
                        val before = str.substring(0, from)
                        i += 1
                        position += before.length
                        entries.add(i2++, TextImpl(before))
                    }

                    val tokenEntries: MutableList<out Node>
                    val inside = pattern.inside()
                    val hasInside = inside != null
                    if (hasInside) {
                        tokenEntries = tokenize(match, inside)
                    } else {
                        tokenEntries = mutableListOf<TextImpl>(TextImpl(match))
                    }

                    entries.add(
                        i2++, SyntaxImpl(
                            token.name(),
                            tokenEntries,
                            pattern.alias(),
                            match,
                            greedy,
                            hasInside
                        )
                    )

                    // important thing here (famous off-by one error) to check against full length (not `length - 1`)
                    if (to < str.length) {
                        val after = str.substring(to)
                        entries.add(i2, TextImpl(after))
                    }

                    if (deleteCount != 1) {
                        matchGrammar(text, entries, grammar, i, position, true, token)
                    }

                    if (oneShot) {
                        break
                    }
                    position += entries.get(i).textLength()
                    ++i
                }
            }
        }
    }

    companion object {
        /**
         * Factory method to create a [Grammar]
         *
         * @param name   of the defined grammar
         * @param tokens a list of [Token]s
         * @return an instance of [Grammar]
         */
        fun grammar(name: String, tokens: MutableList<Token>): Grammar {
            return GrammarImpl(name, tokens)
        }

        fun grammar(name: String, vararg tokens: Token?): Grammar {
            return GrammarImpl(name, tokens.filterNotNull().toMutableList())
        }

        fun token(name: String, patterns: List<Pattern>): Token {
            return TokenImpl(name, patterns.toMutableList())
        }

        fun token(name: String, vararg patterns: Pattern): Token {
            return TokenImpl(name, patterns.toMutableList())
        }

        fun pattern(
            regex: String,
            lookbehind: Boolean = false,
            greedy: Boolean = false,
            alias: String? = null,
            inside: Grammar? = null,
        ): Pattern = pattern(regex.toRegex(), lookbehind, greedy, alias, inside)

        fun pattern(
            regex: Regex,
            lookbehind: Boolean = false,
            greedy: Boolean = false,
            alias: String? = null,
            inside: Grammar? = null,
        ): Pattern {
            return PatternImpl(regex, lookbehind, greedy, alias, inside)
        }


        private fun isSyntaxNode(node: Node): Boolean {
            return node.isSyntax
        }

        private fun isGreedyNode(node: Node): Boolean {
            return node.isSyntax && (node as Syntax).greedy()
        }
    }
}
